﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using Red.Controls.ViewModels;
using Red.Core.Extensions;
using Red.EngineData.View;

namespace Red.Controls
{
    public class InteropTree : RedTree
    {
        private InteropTreeItem _rootTreeItem;

        private Dictionary< ulong, InteropTreeItem > _idToNodeMap = new Dictionary<ulong, InteropTreeItem>();
        private Dictionary< ulong, List<InteropTreeItem>> _parentIdToChildren = new Dictionary<ulong, List<InteropTreeItem>>();

        private Notifier _onChangedNotifier;
        private Task _modifierTask;
        private ConcurrentQueue<string[]> _msgQueue = new ConcurrentQueue<string[]>();

        #region C-tor

        public InteropTree()
        {
            DataContextChanged += ( object sender, DependencyPropertyChangedEventArgs args ) =>
                {
                    var item = DataContext as InteropDataItem;

                    if ( item != null )
                        Rebuild( item );
                };
        }

        #endregion C-tor

        /// <summary>
        ///     Returns node with given id if it exists in the tree
        /// </summary>
        public InteropTreeItem GetNode( ulong id )
        {
            if ( _idToNodeMap.ContainsKey( id ) )
            {
                return _idToNodeMap[id];
            }
            return null;
        }

        public InteropTreeItem GetParent( ulong id )
        {
            var node = GetNode( id );
            if ( node != null )
            {
                foreach ( var el in _parentIdToChildren )
                {
                    if ( el.Value.Contains( node ) )
                    {
                        return GetNode( el.Key );
                    }
                }
            }
            return null;
        }

        /// <summary>
        ///     Resets root element of the tree and registers for update notification
        ///     NOTE: root is automatically expanded
        /// </summary>
        public async void Rebuild( InteropDataItem treeDataItem )
        {
            var rootId = await treeDataItem.ExecuteAsync<ulong>( "root" );

            _rootTreeItem = await AddNode( treeDataItem, 0, rootId.Value );
            _rootTreeItem.IsExpanded = true;

            ItemsSource = new InteropTreeItem[] { _rootTreeItem };

            treeDataItem.Data.RequestNotifier( "_onChanged",
                notifier =>
                {
                    _onChangedNotifier = notifier;
                    _onChangedNotifier.OnNotifyEvent +=
                        msg => // e.g. "[12,a23,r21,a31][13,a41]"
                        {
                            if ( msg.Length == 0 )
                                return;

                            _msgQueue.Enqueue( ParseMessage( msg ) );
                            HandleIncomingMessages( treeDataItem );
                            Refresh().DoNotAwait();
                        };

                    treeDataItem.Data.Execute( Interop.MakeCall ( "_notifierRegistered" ) );
                } );
        }

        private string[] ParseMessage( string message )
        {
            // message : "[12,a23,r21,a31][13,a41]"
            string[] msgParts = message.Split( new string[] { "][" }, StringSplitOptions.RemoveEmptyEntries );
            // msgParts : ["[12,a23,r21,a31","13,a41]"]
            msgParts[0] = msgParts[0].Remove( 0, 1 ); // remove '['
            var lastPart = msgParts[msgParts.Length - 1];
            msgParts[msgParts.Length - 1] = lastPart.Remove( lastPart.Length - 1, 1 ); // remove ']'
            // msgParts : ["12,a23,r21,a31","13,a41"]
            return msgParts;
        }

        private void HandleIncomingMessages( InteropDataItem treeDataItem )
        {
            if ( _modifierTask == null || _modifierTask.IsCompleted )
            {
                _modifierTask = new Task(
                    () =>
                    {
                        bool isRunning = true;
                        while ( isRunning )
                        {
                            string[] msgParts;
                            if ( _msgQueue.TryDequeue( out msgParts ) )
                            {
                                foreach ( var part in msgParts )
                                {
                                    string[] ids = part.Split( ',' );                // ids : ["12","a23","r21","a31"]
                                    ulong parentId = ulong.Parse( ids[0] );              // nodeId : 12
                                    for ( int i = 1; i < ids.Length; ++i )
                                    {
                                        var id = ids[ i ];                           // id : "a23"
                                        ulong nodeId = ulong.Parse( id.Substring( 1 ) ); // nodeId : 23
                                        if ( id.StartsWith( "a" ) )
                                        {
                                            AddNode( treeDataItem, parentId, nodeId ).DoNotAwait();
                                        }
                                        else if ( id.StartsWith( "r" ) )
                                        {
                                            RemoveNode( parentId, nodeId );
                                        }
                                    }
                                }
                            }

                            // stop the task when message queue is emptied
                            if ( _msgQueue.IsEmpty )
                            {
                                isRunning = false;
                            }
                        }
                    }
                );
                _modifierTask.Start();
            }
        }

        private async Task<InteropTreeItem> AddNode( InteropDataItem treeDataItem, ulong parentId, ulong nodeId )
        {
            if ( treeDataItem == null )
                return null;

            var dataItem = treeDataItem.Data.GetSubView( string.Format( "nodes[\"{0}\"].data", nodeId ) );
            await dataItem.Refresh();
            var treeItem = new InteropTreeItem( dataItem, treeDataItem, nodeId );

            {
                // if currently added node has cached children then attach them and clear the cache
                List<InteropTreeItem> children;
                if ( _parentIdToChildren.TryGetValue( nodeId, out children ) )
                {
                    foreach ( var child in children )
                        treeItem.AddChild( child );
                    _parentIdToChildren.Remove( nodeId );
                    // TODO: there's no need for scheduling a rebuild if previous one hasn't been completed
                    Application.Current.Dispatcher.Invoke( () => treeItem.RebuildAsync( false ).DoNotAwait() );
                }
            }

            _idToNodeMap.Add( nodeId, treeItem );

            // add child
            if ( parentId > 0 )
            {
                InteropTreeItem parent;
                if ( _idToNodeMap.TryGetValue( parentId, out parent ) )
                {
                    parent.AddChild( treeItem );
                    // TODO: there's no need for scheduling a rebuild if previous one hasn't been completed
                    Application.Current.Dispatcher.Invoke( () => parent.RebuildAsync( false ).DoNotAwait() );
                }
                else
                {
                    // node was added before its parent, remember it for later
                    List<InteropTreeItem> children;
                    if ( !_parentIdToChildren.TryGetValue( parentId, out children ) )
                    {
                        children = new List<InteropTreeItem>();
                        _parentIdToChildren.Add( parentId, children );
                    }
                    children.Add( treeItem );
                }
            }

            return treeItem;
        }

        private void RemoveNode( ulong parentId, ulong nodeId )
        {
            // remove from map
            var node = _idToNodeMap[ nodeId ];
            _idToNodeMap.Remove( nodeId );

            // remove from parent
            if ( parentId > 0 )
            {
                var parent = _idToNodeMap[ parentId ];
                parent.RemoveChild( nodeId );
                Application.Current.Dispatcher.Invoke( () => parent.RebuildAsync( false ).DoNotAwait() );
            }
        }
    }
}
